#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This application is released under the GNU General Public License 
# v2 (or, at your option, any later version). You can find the full 
# text of the license under http://www.gnu.org/licenses/gpl.txt. 
# By using, editing and/or distributing this software you agree to 
# the terms and conditions of this license. 
# Thank you for using free software!

#  TwitterScreenlet (c) papamitra 2008

# TODO
# - gtk.Entry() sizing
# - run browser

import screenlets
# from screenlets import utils
from screenlets.options import StringOption , BoolOption , IntOption , FileOption , DirectoryOption , ListOption , AccountOption , TimeOption , FontOption, ColorOption , ImageOption
from screenlets.options import create_option_from_node
from screenlets import DefaultMenuItem

import pango
import cairo
import sys
import gtk
import gobject
import os
import re
import twitter
import urllib
import xml.sax.saxutils
import Queue

from ConfigParser import SafeConfigParser

from ThemeParser import ThemeParser

from TwitterBackend import TwitterBackend, TwitterUpdateMethod, Timeline, TwitterResult

import traceback

PI = 3.141596

def escape_for_pango_markup(string):
    # escapes < > & ' "
    # for pango markup not to break
    if string is None:
        return
    if gtk.pygtk_version >= (2, 8, 0) and gtk.gtk_version >= (2, 8, 0):
        escaped_str = gobject.markup_escape_text(string)
    else:
        escaped_str = xml.sax.saxutils.escape(string, {"'": '&apos;',
                                                       '"': '&quot;'})

    return escaped_str

class TwitterScreenlet (screenlets.Screenlet):
    """A screenlet to show Twitter timeline"""

    # default meta-info for Screenlets (should be removed and put into metainfo)
    __name__	= 'TwitterScreenlet'
    __version__	= '0.2.0'
    __author__	= 'papamitra@gmail.com'
    __desc__	= __doc__	# set description to docstring of class

    __timeout = None
    
    __fixed = None
    __entry = None

    __status_on_tooltip = None

    __twitter = None

    item_font = "Sans Medium 5"
    account = ('', '')
    layout = None

    __timeline = Timeline()

    show_public_timeline = True
    num_of_statuses = 5
    update_interval = 5

    header_height = 30
    footer_height = 35
    user_icon_size = 30
    status_height = 48

    window_x = 10
    window_y = 10

    window_width = 200

    #for scroll
    statuses_offset = 0
    _buffer = None
    scroll_width = 20

    Qin = Queue.Queue()

    def __init__ (self, **keyword_args):
        screenlets.Screenlet.__init__(self, width=200, height=200, 
                                      uses_theme=True, **keyword_args)
        if not self.theme:
            self.theme_name="default"

        self.add_options_group('Twitter', 'The twitter widget settings')

        self.add_option(BoolOption('Twitter', 'show_public_timeline', self.show_public_timeline,
                                   'Show Public Timeline',''), realtime=False)

        self.add_option(AccountOption('Twitter', 'account', 
                                      self.account, 'Username/Password', 
                                      'Enter username/password here ...'))
        
        self.add_option(IntOption('Twitter', 'update_interval', 
                                  self.update_interval, 'Update interval', 
                                  'The interval for refreshing status (in minutes)', min=1, max=60))

        self.add_option(IntOption('Twitter', 'num_of_statuses', 
                                  self.num_of_statuses, 'Number of statuses to display', 
                                  'Number of statuses to display',  min=1, max=20))

        self.add_option(IntOption('Twitter', 'status_height',
                                  self.status_height, 'Height of status to display', 
                                  'Height of status to display',  min=1, max=99))

        self.notifier = screenlets.utils.Notifier(self)

    def __setattr__(self, name, value):
        # call Screenlet.__setattr__ in baseclass (ESSENTIAL!!!!)
        screenlets.Screenlet.__setattr__(self, name, value)
        if name == "update_interval":
            if value > 0:
                self.__dict__['update_interval'] = value
                if self.__timeout:
                    gobject.source_remove(self.__timeout)
                self.__timeout = gobject.timeout_add(int(value * 60000), self.update)
            else:
                self.__dict__['update_interval'] = 1

        if name == 'account':

            if self.account[0] == '':
                self.__twitter = TwitterBackend(self, TwitterUpdateMethod.PUBLIC)
                
                self.show_public_timeline = True
            else:
                if self.show_public_timeline:
                    self.__twitter = TwitterBackend(self, TwitterUpdateMethod.PUBLIC)
                else:
                    self.__twitter = TwitterBackend(self, TwitterUpdateMethod.FRIENDS)

            self.__twitter.get_timeline_start()

        if name == 'show_public_timeline':
            if self.__twitter:
                if self.show_public_timeline == False and self.account[0] != '':
                    self.__twitter.set_update_method(TwitterUpdateMethod.FRIENDS)
                else:
                    self.__twitter.set_update_method(TwitterUpdateMethod.PUBLIC)
                
                self.__twitter.get_timeline_start()

        if name == 'num_of_statuses':
            self.resize_and_redraw()

        if name == 'status_height':
            if value < 1:
                self.__dict__['status_height'] = 1
            
            for status in self.__timeline.statuses:
                status.buffer = None
            self.resize_and_redraw()

    def resize_and_redraw(self):
        if not self.layout: return
        if not self.__twitter: return

        for status in self.__timeline.statuses:
            status.buffer = None

        self.window_x = self.layout.window.shadow_size
        self.window_y = 0

        self.window_width = self.layout.window.width
        self.window_height = self.header_height + self.footer_height + self.statuses_height()
        self.width = self.window_width + self.layout.window.shadow_size * 2
        self.height = self.window_height + self.layout.window.shadow_size
        self.modify_entry_pos()

        self.update_buffer()
        self.redraw_canvas()
        
    def get_border_width(self):
        if not self.layout: return 0
        ls = self.layout.status
        if ls.enable_border:
            return ls.border_width
        else:
            return 0

    def statuses_height(self):
        return self.status_height * self.num_of_statuses + self.get_border_width() * (self.num_of_statuses -1)

    def modify_entry_pos(self):
        #move text entry
        if self.__entry:
            self.__entry.set_size_request(int(self.window_width - 20 * self.scale), -1)

        if self.__fixed:
            self.__fixed.move(self.__entry, int((10 + self.window_x) * self.scale), int((self.window_y + self.header_height + self.statuses_height() + 5) * self.scale))
        self.window.show_all()        

    def update(self):
        if self.__twitter:
            self.__twitter.get_timeline_start()

        return True # timeout contintue

    def on_after_set_atribute(self,name, value):
        """Called after setting screenlet atributes"""
        #print name + ' is going to change from ' + str(value)
        pass

    def on_before_set_atribute(self,name, value):
        """Called before setting screenlet atributes"""
        #print name + ' has changed to ' + str(value)
        pass


    def on_create_drag_icon (self):
        """Called when the screenlet's drag-icon is created. You can supply
        your own icon and mask by returning them as a 2-tuple."""
        return (None, None)

    def on_composite_changed(self):
        """Called when composite state has changed"""
        pass

    def on_drag_begin (self, drag_context):
        """Called when the Screenlet gets dragged."""
        pass

    def on_drag_enter (self, drag_context, x, y, timestamp):
        """Called when something gets dragged into the Screenlets area."""
        pass

    def on_drag_leave (self, drag_context, timestamp):
        """Called when something gets dragged out of the Screenlets area."""
        pass

    def on_drop (self, x, y, sel_data, timestamp):
        """Called when a selection is dropped on this Screenlet."""
        return False

    def on_focus (self, event):
        """Called when the Screenlet's window receives focus."""
        pass

    def on_hide (self):
        """Called when the Screenlet gets hidden."""
        pass

    def on_init (self):
        """Called when the Screenlet's options have been applied and the 
        screenlet finished its initialization. If you want to have your
        Screenlet do things on startup you should use this handler."""
        print 'i just got started'
        # add  menu items from xml file
        self.add_default_menuitems(DefaultMenuItem.XML)
        # add menu item
        self.add_menuitem("refresh", "Refresh")
        # add default menu items
        self.add_default_menuitems()

        self.__entry = gtk.Entry(140)
        self.__entry.set_size_request(self.window_width,-1)
        self.__entry.set_has_frame(False)
        self.__fixed = gtk.Fixed()
        self.window.add(self.__fixed)
        self.__fixed.put(self.__entry, 0, 0)

        self.__tooltips = gtk.Tooltips()
        self.__tooltips.set_tip(self.__entry, "<Shift>Return to Post")

        self.modify_entry_pos()
        if not self.__twitter:
            self.__twitter = TwitterBackend(self, TwitterUpdateMethod.PUBLIC)

        self.update()
        self.update_interval = self.update_interval

        self.window_height = self.header_height + self.footer_height + self.statuses_height()

    def on_key_down(self, keycode, keyvalue, event):
        """Called when a keypress-event occured in Screenlet's window."""

        key = gtk.gdk.keyval_name(event.keyval)

        if key == "Return" and (event.state & gtk.gdk.SHIFT_MASK):
            #screenlets.show_message(self, self.__entry.get_text())
            if self.__twitter:
                self.__twitter.post_message_start(self.__entry.get_text())
                self.__entry.set_text("")
                self.__twitter.get_timeline_start()

    def on_load_theme (self):
        """Called when the theme is reloaded (after loading, before redraw)."""
        options_file = sys.path[0]+"/themes/"+self.theme_name+"/options.conf"
        if os.path.exists(options_file): 
            self.layout = ThemeParser(options_file)
        else:
            raise "Invalid Theme. No options.conf"

        self.resize_and_redraw()

    def on_menuitem_select (self, id):
        """Called when a menuitem is selected."""
        if id == "refresh":
            print "refresh"
            self.update()

    def on_mouse_down (self, event):
        """Called when a buttonpress-event occured in Screenlet's window. 
        Returning True causes the event to be not further propagated."""

        return False

    def on_mouse_enter (self, event):
        """Called when the mouse enters the Screenlet's window."""
        #print 'mouse is over me'

    def on_mouse_leave (self, event):
        """Called when the mouse leaves the Screenlet's window."""
        self.hide_tooltip()
        self.hover = False
        self.__status_on_tooltip = None
        #print 'mouse leave'

    def on_mouse_move(self, event):
        """Called when the mouse moves in the Screenlet's window."""

        def get_status_under_pointer(x, y):
            try:
                if y > self.header_height and y < self.window_height - self.footer_height:
                    # FIXME:
                    return self.__timeline.statuses[int((y - self.header_height + self.statuses_offset)/ (self.status_height + self.get_border_width()) )]
                else:
                    return None
            except:
                return None

        status = get_status_under_pointer(event.x, event.y)
        if status and status != self.__status_on_tooltip:
            self.show_tooltip("%s\n%s" % (status.GetUser().GetScreenName(), 
                                          escape_for_pango_markup(status.GetText()))
                              ,self.x+self.mousex,self.y+self.mousey)
            self.hover = True
            self.__status_on_tooltip = status
        elif status == None:
            self.hide_tooltip()
            self.hover = False
            self.__status_on_tooltip = None
            
        #self.redraw_canvas()

    def on_mouse_up (self, event):
        """Called when a buttonrelease-event occured in Screenlet's window. 
        Returning True causes the event to be not further propagated."""
        return False

    def on_quit (self):
        """Callback for handling destroy-event. Perform your cleanup here!"""
        # clear timeout
        if self.__timeout:
            gobject.source_remove(self.__timeout)

    def on_realize (self):
        """"Callback for handling the realize-event."""
        pass

    def on_scale (self):
        """Called when Screenlet.scale is changed."""
        self.resize_and_redraw()

    def on_scroll_up (self):
        """Called when mousewheel is scrolled up (button4)."""
        self.statuses_offset -= self.scroll_width
        if self.statuses_offset < 0 : self.statuses_offset = 0
        self.redraw_canvas()

    def on_scroll_down (self):
        """Called when mousewheel is scrolled down (button5)."""
        self.statuses_offset += self.scroll_width
        if self.statuses_offset > self.buffer_height - self.statuses_height() : self.statuses_offset = self.buffer_height - self.statuses_height()
        self.redraw_canvas()

    def on_show (self):
        """Called when the Screenlet gets shown after being hidden."""
        pass

    def on_switch_widget_state (self, state):
        """Called when the Screenlet enters/leaves "Widget"-state."""
        pass

    def on_unfocus (self, event):
        """Called when the Screenlet's window loses focus."""
        pass

    def on_draw (self, ctx):
        """In here we load the theme"""

        try:
            self.__timeline = self.Qin.get(False)
            self.resize_and_redraw()
            return
        except:
            #Queue is Empty
            pass

        if not self.layout: return

        l = self.layout
        ctx.save()
        ctx.scale(self.scale , self.scale)
        ctx.translate(self.window_x, self.window_y)

        #draw shadow
        if l.window.shadowed:
            shadow_size = l.window.shadow_size
            self.draw_shadow(ctx, -self.window_x, -self.window_y, self.window_width, self.window_height ,shadow_size,l.window.shadow_color)

        # draw header
        ctx.save()
        self.round_rectangle(ctx, 0, 0, self.window_width, self.header_height, l.window.radius, l.window.radius, 0, 0)
        ctx.clip()
        linear = cairo.LinearGradient(0, 0, 0, self.header_height)
        linear.add_color_stop_rgba(0, *l.header.start_color.get())
        linear.add_color_stop_rgba(1.0,*l.header.end_color.get())
        ctx.set_source(linear)
        ctx.paint()
        ctx.restore()

        if self.show_public_timeline == False and self.account[0] != '': header_str = "Friends" 
        else: header_str = "Public"

        ctx.save()
        ctx.set_source_rgba(*l.header.title_color.get())
        self.draw_text(ctx, header_str, 10, 5, self.item_font, 14, self.window_width,
                       allignment=pango.ALIGN_LEFT,
                       weight=5,
                       ellipsize=pango.ELLIPSIZE_NONE)
        ctx.restore()

        # draw twitter logo
        #ctx.save()
        #ctx.translate(120,6)
        #ctx.scale(.4,.4)
        #self.theme.render(ctx, 'logo')
        #ctx.restore()

        # draw status buffer(refreshed by update_buffer())
        ctx.save()
        if self._buffer:
            ctx.translate(0,self.header_height)
            ctx.set_source_rgba(0,0,0,0)
            ctx.rectangle(0,0,self.window_width,self.statuses_height())
            ctx.clip()
            ctx.set_operator(cairo.OPERATOR_OVER)
            ctx.set_source_pixmap(self._buffer, 0, -self.statuses_offset)
            ctx.paint()
        ctx.restore()

        # draw footer
        ctx.save()
        ctx.translate(0, self.header_height + self.statuses_height())
        self.round_rectangle(ctx, 0, 0, self.window_width, self.footer_height, 0, 0, l.window.radius, l.window.radius)
        ctx.clip()
        linear = cairo.LinearGradient(0, 0, 0, self.footer_height)
        linear.add_color_stop_rgba(0.0, *l.footer.start_color.get())
        linear.add_color_stop_rgba(1.0, *l.footer.end_color.get())
        ctx.set_source(linear)
        ctx.paint()
        ctx.restore()

        ctx.restore()

    def on_draw_shape (self, ctx):
        self.on_draw(ctx)

    def on_get_timeline (self, backend, timeline, result):
        """ call by TwitterBackend thread"""
        if TwitterResult.ERROR == result:
            self.notifier.notify("Failed to get timeline")
            return

        self.Qin.put(timeline)
        # call on_draw()
        self.window.queue_draw()

    def on_post_message (self, backend, result):
        """ call by TwitterBackend thread"""
        if TwitterResult.ERROR == result:
            self.notifier.notify("Failed to post message")
            return

    def update_buffer(self):
        if not self.window.window: return
        if not self.layout: return

        self.statuses_offset = 0

        status_num = max(self.num_of_statuses, len(self.__timeline.statuses))
        self.buffer_height = status_num * self.status_height + (status_num -1) * self.get_border_width()

        self._buffer = gtk.gdk.Pixmap(self.window.window, 
                                    int(self.window_width), int(self.buffer_height), -1)

        ctx = self._buffer.cairo_create()
        self.clear_cairo_context(ctx)
        ctx.set_source_rgba(1,1,1,1)

        self.draw_status_bg(ctx, status_num)

        ctx.save()
        for status in self.__timeline.statuses:
            self.draw_user_status(ctx, status)
            ctx.translate(0, self.status_height)
            ctx.translate(0, self.get_border_width())
        ctx.restore()


    def draw_text(self, ctx, text, x, y,  font, size, width, allignment=pango.ALIGN_LEFT,alignment=None,justify = False,weight = 0, ellipsize = pango.ELLIPSIZE_NONE, is_single_p=False):
        """Draws text"""

        ctx.save()
        ctx.translate(x, y)
        if self.p_layout == None :
            self.p_layout = ctx.create_layout()
        else:
            ctx.update_layout(self.p_layout)
        if self.p_fdesc == None:self.p_fdesc = pango.FontDescription()
        else: pass
        self.p_fdesc.set_family_static(font)
        self.p_fdesc.set_size(size * pango.SCALE)
        self.p_fdesc.set_weight(weight)
        self.p_layout.set_font_description(self.p_fdesc)
        self.p_layout.set_width(width * pango.SCALE)
        self.p_layout.set_alignment(allignment)
        if alignment != None:self.p_layout.set_alignment(alignment)
        self.p_layout.set_justify(justify)
        self.p_layout.set_ellipsize(ellipsize)
        self.p_layout.set_markup(text)
        ###
        self.p_layout.set_single_paragraph_mode(False)
        self.p_layout.set_wrap(pango.WRAP_WORD_CHAR)
        ###

        ctx.show_layout(self.p_layout)
        ctx.restore()

    def draw_status_bg(self, ctx, status_num):
        """ draw status background"""
        
        if not self.layout: return

        ls = self.layout.status
        ctx.save()
        for index in range(status_num):
            if  index % 2: # index 0 = line 1
                ctx.set_source_rgba(*ls.even_line_color.get())
            else:
                ctx.set_source_rgba(*ls.odd_line_color.get())
                
            self.draw_rectangle(ctx,0,0,self.window_width,self.status_height)           

            ctx.translate(0, self.status_height)
            if ls.enable_border and index < status_num - 1:
                ctx.save()
                ctx.set_source_rgba(*ls.border_color.get())
                self.draw_rectangle(ctx,0,0,self.window_width, self.get_border_width())
                ctx.restore()
                ctx.translate(0, self.get_border_width())

        ctx.restore()

    def draw_user_status (self, ctx, status):
        """ draw user status"""

        if not self.window.window:
            return

        if not self.layout.status:
            return

        ls = self.layout.status

        if status.buffer == None:
            buffer = gtk.gdk.Pixmap(self.window.window, 
                                    int(self.window_width), int(self.status_height), -1)

            text_ctx = buffer.cairo_create()
            self.clear_cairo_context(text_ctx)
            text_ctx.set_source_rgba(1,1,1,1)

            status_format = "<span foreground=\"%s\"><b>%s</b></span> <span foreground=\"%s\">%s</span>"
            status_text =  status_format % (ls.name_color.get_str(), 
                                            status.GetUser().GetScreenName(),
                                            ls.text_color.get_str(),
                                            escape_for_pango_markup(status.GetText()))


            self.draw_text(text_ctx, status_text, 
                           self.user_icon_size + 4, 2, 
                           self.item_font, 9, 
                           self.window_width-5-self.user_icon_size, 
                           allignment=pango.ALIGN_LEFT, 
                           weight=5,
                           ellipsize=pango.ELLIPSIZE_NONE)
            status.buffer = buffer

        ctx.save()
        #ctx.set_source_rgba(0,0,0,0)
        #ctx.rectangle(0,0,self.window_width, self.status_height)
        #ctx.clip()
        ctx.set_operator(cairo.OPERATOR_OVER)
        ctx.set_source_pixmap(status.buffer, 0, 0)
        ctx.paint()
        ctx.restore()

        self.draw_user_icon(ctx, 2, 2, self.user_icon_size, self.user_icon_size, 4.0, 1, self.__timeline.users[status.GetUser().GetName()])

    # from PidginScreenlet
    # Draw User Icon (Display Picture) 
    def draw_user_icon (self, ctx, x, y, w, h, rounding_radius, transparency, user):
        if user.iconPixbuf:
            ctx.save()
            ctx.translate(x, y)
            r = rounding_radius
            if r: self.round_rectangle(ctx,0,0,w,h,r,r,r,r)
            else: ctx.rectangle(0,0,w,h)
            ctx.clip()
            ctx.set_source_pixbuf(user.iconPixbuf, 0, 0)
            ctx.paint_with_alpha(transparency)
            #ctx.paint()
            ctx.restore()

    # from PidginScreenlet
    # r1: top-left radius, r2: top-right radius, 
    # r3: bottom-left radius, r4: bottom-right radius
    def round_rectangle(self, ctx, x, y, width, height, r1, r2, r3, r4):
        ctx.move_to(x+r1, y)
        ctx.line_to(x+width-r2,y)
        # top right
        if r2 > 0:
                ctx.arc(x+width-r2, y+r2, r2, -PI/2.0, 0)
        ctx.line_to(x+width,y+height-r4)
        # bottom right
        if r4 > 0:
                ctx.arc(x+width-r4, y+height-r4, r4, 0, PI/2.0)
        ctx.line_to(x+r3, y+height)
        # bottom left
        if r3 > 0:
                ctx.arc(x+r3, y+height-r3, r3, PI/2.0, PI)
        ctx.line_to(x,y+r1)
        # top left
        if r1 > 0:
                ctx.arc(x+r1, y+r1, r1, PI, PI*1.5)
        
if __name__ == "__main__":
    # create new session
    import screenlets.session
    screenlets.session.create_session(TwitterScreenlet, threading=True)
